package com.example.lotterysystem.service.mq;

import cn.hutool.core.date.DateUtil;
import com.example.lotterysystem.common.exception.ServiceException;
import com.example.lotterysystem.common.utils.JacksonUtil;
import com.example.lotterysystem.common.utils.MailUtil;
import com.example.lotterysystem.controller.param.DrawPrizeParam;
import com.example.lotterysystem.mapper.ActivityPrizeMapper;
import com.example.lotterysystem.mapper.WinningRecordMapper;
import com.example.lotterysystem.pojo.dataobject.ActivityPrizeDO;
import com.example.lotterysystem.pojo.dataobject.WinningRecordDO;
import com.example.lotterysystem.service.DrawPrizeService;
import com.example.lotterysystem.service.activitystatus.ActivityStatusManager;
import com.example.lotterysystem.service.dto.ConvertActivityStatusDTO;
import com.example.lotterysystem.service.enums.ActivityPrizeStatusEnum;
import com.example.lotterysystem.service.enums.ActivityPrizeTiersEnum;
import com.example.lotterysystem.service.enums.ActivityStatusEnum;
import com.example.lotterysystem.service.enums.ActivityUserStatusEnum;
import com.example.lotterysystem.service.impl.DrawPrizeServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.example.lotterysystem.common.config.DirectRabbitConfig.QUEUE_NAME;

/**
 * 接收队列中的信息进行处理
 */
@Component
// 这里传入的是一个 RabbitMq 的名称，直到接受那个发送方的信息
@RabbitListener(queues = QUEUE_NAME)
public class MqReceiver {
    // 日志
    private static final Logger logger = LoggerFactory.getLogger(MqReceiver.class);

    @Autowired
    private DrawPrizeService drawPrizeService;
    @Autowired
    private ActivityStatusManager activityStatusManager;
    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    @Autowired
    private MailUtil mailUtil;
    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;
    @Autowired
    private WinningRecordMapper winningRecordMapper;


    // 消费者
    @RabbitHandler
    public void process(Map<String,String> message) {
        // 成功接收到队列中的消息
        logger.info("MQ成功接收到消息，message:{}",
                JacksonUtil.writeValueAsString(message));
        // 从 Map中拿到对应的消息
        String paramString = message.get("messageData");
        // 进行反序列化
        DrawPrizeParam param = JacksonUtil.readValue(paramString, DrawPrizeParam.class);
        // 进行详细的处理
        try {
            // 检验抽奖请求是否有效
            // 1、可能存在前端发送两个一样的抽奖请求，对于param来说也是一样的两个请求
            // 2、param在处理最后一个奖项的时候，前端发送两个一样的请求
            // 那么就会出现 : 第一次的请求通过: 处理param1 -> 活动完成，奖品完成，状态进行转变
            //             第二次的请求的时候: 处理param2 -> 出现异常，抛出的话，就会进行回滚操作，使状态进行转换，这是错误的
            // 所以这里的方法需要返回 boolean类型 进行判断，是否有效，不能进行抛出异常的方式说明无效
            if (!drawPrizeService.checkDrawPrizeParam(param)) {
                // 出现错误直接进行过返回
                return;
            }

            // 状态的扭转 (非常重要，设计到设计模式)
            statusConvert(param);

            // 保存中奖者名单，并且进行返回，用于后序的中奖者通知
            // 保存到数据库中的中奖者的表中，并且需要进行缓存操作，缓存到Redis中，并且一个是奖品维度，一个是活动维度的保存
            List<WinningRecordDO> winningRecordDOList =
                    drawPrizeService.saveWinnerRecords(param);


            // 中奖者通知，正常应该是使用 并发处理，是手机短信和邮箱共同发送，但是没有手机短信，这里只有邮箱
            // 这里只使用邮箱，但是存在 并发处理，为了后序的扩展
            syncExecute(winningRecordDOList);

        }catch (ServiceException e) {
            logger.error("处理 MQ 消息异常! {}:{}",e.getCode(),e.getMessage(),e);
            // 需要进行回滚操作，保证数据的一致性
            rollback(param);
            // 抛出异常，使 RabbitMQ 造成 消息重试（解决异常：代码bug、网络问题、服务问题）
            throw e;

        }catch (Exception e) {
            logger.error("处理 MQ 消息异常!",e);
            // 需要进行回滚操作，保证数据的一致性
            rollback(param);
            // 抛出异常，使 RabbitMQ 造成 消息重试（解决异常：代码bug、网络问题、服务问题）
            throw e;
        }

    }

    /**
     * 处理抽奖异常的回滚行为：恢复处理请求之前的库表状态
     * @param param
     */
    private void rollback(DrawPrizeParam param) {
        // 1、回滚状态：活动、奖品、人员
        // 状态是否需要回滚
        if (!statusNeedRollback(param)) {
            // 不需要
            return;
        }
        // 需要，进行回滚
        rollbackStatus(param);

        // 2、回滚中奖名单
        // 是否需要回滚
        if (!winnerNeedRollback(param)) {
            // 不需要
            return;
        }
        // 需要回滚
        rollbackWinner(param);
    }

    /**
     * 将获奖状态进行回滚操作
     * 回滚中奖记录：删除奖品下的中奖者
     * @param param
     */
    private void rollbackWinner(DrawPrizeParam param) {
        drawPrizeService.deleteRecords(param.getActivityId(), param.getPrizeId());
    }

    /**
     * 判断是否需要回滚
     * @param param
     * @return
     */
    private boolean winnerNeedRollback(DrawPrizeParam param) {
        // 判断活动中的奖品是否存在中奖者
        // 存在就需要回滚操作
        int count = winningRecordMapper.count(param.getActivityId(),param.getPrizeId());
        return count > 0;
    }

    /**
     * 恢复相关状态
     * @param param
     */
    private void rollbackStatus(DrawPrizeParam param) {
        // 就是重新构造一个 ConvertActivityStatusDTO 状态进行修改成 没有完成的状态，之后进行使用 策略模式进行修改即可
        ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
        convertActivityStatusDTO.setActivityId(param.getActivityId());
        convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.RUNNING);
        convertActivityStatusDTO.setPrizeId(param.getPrizeId());
        convertActivityStatusDTO.setTargetPrizeStatus(ActivityPrizeStatusEnum.INIT);
        convertActivityStatusDTO.setUserId(
                param.getWinnerList().stream()
                        .map(DrawPrizeParam.Winner::getUserId)
                        .collect(Collectors.toList())
        );
        convertActivityStatusDTO.setTargetUserStatus(ActivityUserStatusEnum.INIT);
        activityStatusManager.rollbackHandlerEvent(convertActivityStatusDTO);
    }

    /**
     * 检验活动、奖品、人员 是否需要回滚
     * @param param
     * @return
     */
    private boolean statusNeedRollback(DrawPrizeParam param) {
        // 判断 活动 + 人员 + 奖品 相关的状态是否进行了扭转 (正常思路)
        // 但是在扭转状态的时候呢，保证了事务的一致性，要么就都扭转了，要么就都没有进行扭转(不包含活动)
        // 因此，只需要判断 人员/奖品 是否扭转过，就能判断出状态是否全部扭转
        // 不能判断活动是否已经扭转
        // 结论: 判断 人员/奖品 的状态是否扭转，就可以判断出全部状态是否扭转了
        ActivityPrizeDO activityPrizeDO =
                activityPrizeMapper.selectByAPId(param.getActivityId(),param.getPrizeId());
        return activityPrizeDO.getStatus()
                .equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name());
    }

    /**
     * 并发处理抽奖后续流程，通知信息
     * @param winningRecordDOList
     */
    private void syncExecute(List<WinningRecordDO> winningRecordDOList) {
        // 通过线程池 threadPoolTaskExecutor
        // 扩展：加入策略模式或者其他设计模式来完成后续的异步操作
        // 邮箱通知
        threadPoolTaskExecutor.execute(() -> sendMail(winningRecordDOList));
        // 短信通知
        // 目前不存在
    }

    /**
     * 发送邮箱信息
     * @param winningRecordDOList
     */
    private void sendMail(List<WinningRecordDO> winningRecordDOList) {
        if (CollectionUtils.isEmpty(winningRecordDOList)) {
            logger.info("中奖列表为空，不用发邮件！");
            return;
        }
        for (WinningRecordDO winningRecordDO : winningRecordDOList) {
            // Hi,XXX。恭喜你在抽奖活动活动中获得二等奖:吹风机。获奖奖时间为18:18:44,请尽快领取您的奖励
            String context = "Hi，" + winningRecordDO.getWinnerName() + "。恭喜你在"
                    + winningRecordDO.getActivityName() + "活动中获得"
                    + ActivityPrizeTiersEnum.forName(winningRecordDO.getPrizeTier()).getMessage()
                    + "：" + winningRecordDO.getPrizeName() + "。获奖时间为"
                    + DateUtil.formatTime(winningRecordDO.getWinningTime()) + "，请尽快领 取您的奖励！";
            mailUtil.sendSampleMail(winningRecordDO.getWinnerEmail(),
                    "中奖通知", context);
        }
    }

    /**
     * 状态扭转处理 实操
     * @param param
     */
    private void statusConvert(DrawPrizeParam param) {
        ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
        convertActivityStatusDTO.setActivityId(param.getActivityId());
        convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.COMPLETED);
        convertActivityStatusDTO.setPrizeId(param.getPrizeId());
        convertActivityStatusDTO.setTargetPrizeStatus(ActivityPrizeStatusEnum.COMPLETED);
        convertActivityStatusDTO.setUserId(
                param.getWinnerList().stream()
                        .map(DrawPrizeParam.Winner::getUserId)
                        .collect(Collectors.toList())
        );
        convertActivityStatusDTO.setTargetUserStatus(ActivityUserStatusEnum.COMPLETED);
        activityStatusManager.handlerEvent(convertActivityStatusDTO);
    }

//    /**
//     * 状态扭转处理 说明
//     * @param param
//     */
//    private void statusConvert(DrawPrizeParam param) {
//        // 这里存在一些问题:
//        // 1、活动状态扭转存在依赖性，导致代码维护性差，活动扭转需要奖品都结束，才能进行扭转
//        // 2、状态扭转条件可能存在需要扩展的情况，如果按照下述的写法呢，就存在扩展性差，维护性差的情况
//        // 3、这样会使代码的灵活性、维护性、可扩展性变差
//        // 解决方法: 责任链设计模式(解决1) 策略模式(解决2)
//
//        // 活动：RUNNING -> COMPLETED 需要先将活动中所有的奖品都进行改变状态后才可以进行修改
//        // 奖品：INIT-->COMPLETED
//        // 人员列表: INIT-->COMPLETED
//
//        // 1、扭转奖品状态
//        // 先查询活动关联的奖品信息
//        // 之后条件判断是否符合扭转奖品状态: 就是判断当前奖品的状态 是否是COMPLETED  如果不是，就需要扭转状态
//
//        // 2、扭转人员状态
//        // 先查询活动关联的人员信息
//        // 之后条件判断是否符合扭转人员状态: 就是判断当前人员的状态 是否是COMPLETED  如果不是，就需要扭转状态
//
//        // 4、当需要新增扭转状态的时候，需要再活动扭转之前进行扭转
//        //    扭转xxx状态
//
//        // 3、扭转活动状态 (必须在所有的奖品扭转结束之后才能进行扭转)
//        // 先查询活动信息
//        // 之后条件判断是否符合扭转人员状态:
//        //      就是判断当前人员的状态 是否是COMPLETED  如果不是，
//        //      且所有的奖品扭转结束，就需要扭转状态
//
//        // 5、更新活动完整信息并且进行缓存
//    }
}
